Skip to content

feat(swift-sdk): deleteWallet wipes full wallet footprint#3653

Merged
llbartekll merged 6 commits into
v3.1-devfrom
delete-wallet
May 18, 2026
Merged

feat(swift-sdk): deleteWallet wipes full wallet footprint#3653
llbartekll merged 6 commits into
v3.1-devfrom
delete-wallet

Conversation

@llbartekll
Copy link
Copy Markdown
Contributor

@llbartekll llbartekll commented May 15, 2026

Issue being fixed or feature implemented

A wallet deletion through the SDK left orphan data behind: SwiftData rows the @Relationship cascade graph doesn't reach (PersistentTransaction, PersistentPendingInput, PersistentTokenBalance), in-memory Rust manager state, per-identity Keychain entries, and the shared network sync checkpoint. The next wallet created on the same network observed ghost rows from the deleted one. The same gap was present in the SDK's own example app — its WalletDetailView.deleteWallet() carried a TODO acknowledging the missing PlatformWalletManager removal API.

What was done?

Adds PlatformWalletManager.deleteWallet(walletId:) throws, a single SDK call that wipes a wallet's complete footprint:

  • Rust manager state: a new platform_wallet_manager_remove_wallet FFI drops the wallet from the in-memory map, snapshots its identities, and unregisters them from IdentitySyncManager so per-identity token-balance polling stops.
  • Stale-callback fence (Swift-side row gate): wallet-scoped persister callbacks fetch the PersistentWallet row before writing. ensureWalletRecord creates only inside persistWalletMetadata — the first callback dispatched in any registration round; every other wallet-scoped callback uses findWalletRecord and silently drops when the row is missing. Closes the race where Rust tasks already in flight when the delete fired could resurrect rows.
  • Identity-sync race: IdentitySyncManager::apply_fresh_balances re-checks the live state under its write lock before persisting, so a balance fetched mid-sync for a just-unregistered identity is dropped on the floor.
  • SwiftData wipe: cascades the wallet's identities (schema rule is .nullify; this explicit path takes them down), sweeps PersistentPendingInput by walletId denorm, sweeps PersistentTransaction rows now reachable by nothing, sweeps PersistentTokenBalance for the cascaded identity ids, and deletes the network sync state row only when no sibling wallet remains on that network.
  • Keychain wipe: runs after the SwiftData wipe so the row gate has already taken effect — per-identity privkey_* + specialkey_* rows and wallet-derived identity_privkey.<path> rows, then WalletStorage metadata then mnemonic last (mnemonic-as-retry-anchor — if any earlier step fails, the pre-flight on retry still sees it).

The example app's WalletDetailView becomes the first consumer; dashwallet-ios follows.

How Has This Been Tested?

  • New WalletDeletionTests (Swift, against in-memory ModelContainer): cascade + orphan-tx sweep, idempotency under double-call, network-sync conditional cleanup (both last-wallet and sibling-remains branches), and per-scheme Keychain sweeps with an isolated keychain service.
  • cargo check -p platform-wallet, cargo clippy -p platform-wallet-ffi clean.
  • build_ios.sh --target sim --profile dev succeeds with -warnings-as-errors.
  • All WalletDeletionTests pass under xcodebuild test.

Worth a simulator smoke pass before merging: create a wallet, sync a bit, delete it, create another on the same network, confirm StorageExplorerView shows zero ghosts for the deleted walletId across every Persistent* table.

Breaking Changes

KeychainManager.deleteAllPrivateKeys(for:) and KeychainManager.deleteAllIdentityPrivateKeys(forWalletId:) are now throws (were -> Bool previously, swallowing keychain errors). External consumers — including dashwallet-ios — will need to try the calls.

Conventional-commit type is feat, not feat!, because the breaking surface is limited to two helper methods whose previous Bool return value didn't distinguish "nothing to delete" from "delete failed" — call sites that relied on the boolean were already silently wrong. Happy to retitle to feat! if reviewers prefer.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Added a wallet deletion API that fully wipes a wallet and its associated identities, keychain items, and persisted data.
    • New persistence APIs to fetch identity IDs for a wallet and to delete wallet-scoped persisted data.
  • Behavioral Changes

    • Keychain bulk-deletion APIs now throw on errors and perform more thorough, consistent removals.
  • Tests

    • Added tests covering wallet deletion, persistence cleanup, network sync-state behavior, and keychain sweep isolation.

Review Change Stack

@github-actions github-actions Bot added this to the v3.1.0 milestone May 15, 2026
@llbartekll llbartekll marked this pull request as draft May 15, 2026 13:40
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements end-to-end wallet deletion: Rust FFI and manager removal that unregisters identities, identity-sync early-exit on unregistered identities, Swift persistence APIs to query and delete wallet-scoped data, keychain sweeping, a public Swift deleteWallet API, UI wiring, and tests.

Changes

Wallet Deletion

Layer / File(s) Summary
Rust persistence foundation
packages/rs-platform-wallet-ffi/src/persistence.rs
PersistenceCallbacks now implements Default (context null, callbacks None) and file ends with a trailing newline.
Rust identity sync robustness
packages/rs-platform-wallet/src/manager/identity_sync.rs, tests
Introduces apply_fresh_balances to skip persistence when identity is unregistered; rebuilds token cache from prior tokens; adds RecordingPersister, helper make_recording_manager, and a test ensuring no persistence after unregister.
Rust wallet removal lifecycle
packages/rs-platform-wallet-ffi/src/manager.rs, packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
Adds FFI platform_wallet_manager_remove_wallet, collects owned identity IDs from wallet info, removes wallet, and unregisters each identity asynchronously; treats missing wallet as idempotent success.
Swift persistence handler data access
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
onQueue now rethrows, introduces findWalletRecord non-creating lookup, short-circuits persist paths when wallet row missing, and adds identityIdsForWallet and deleteWalletData with queued cascade deletion and rollback handling.
Swift keychain deletion APIs
packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift
Refactors key deletion to throwing, switches metadata to hex toHexString(), adds deleteAllKeychainItems(forIdentityId:) and deleteAllIdentityPrivateKeys(forWalletId:), and adds deleteGenericPassword(account:) helper.
Swift platform manager deletion API
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
Adds deleteWallet(walletId:) public method that validates inputs, calls Rust FFI removal, cleans in-memory state, queries identities, deletes persisted data, sweeps keychain, and clears metadata then mnemonic storage.
Swift UI integration and test coverage
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift, packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/*
WalletDetailView delegates deletion to the platform manager; adds WalletDeletionTests covering persisted deletion, sync-state retention, and keychain isolation; updates KeyManager test WIF call.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • shumkov
  • QuantumExplorer

🐰 A wallet's final farewell, so neat,
Identities unbound, their sync complete,
Keychains swept clean, no secrets remain,
Swift data whispers down memory's lane,
Deletion done right—no loose ends to greet!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the primary objective: adding a deleteWallet feature that comprehensively removes wallet state from Swift runtime, persistence, and keychain storage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch delete-wallet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "1db9d73627f5f3fe50437d877ddfebe365f41f00adb5e0ece076e8769c40b6eb"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rs-platform-wallet/src/manager/identity_sync.rs (1)

570-587: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid calling external persistence while holding state write lock.

self.persister.store(...) runs under self.state.write(). Because this is an external call, it can stall or re-enter paths that need state, blocking register/unregister/update_watched_tokens and creating deadlock risk.

Take a snapshot under lock, release lock before store, then reacquire and re-check identity before applying cache updates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/manager/identity_sync.rs` around lines 570 -
587, Currently persister.store(...) is called while holding the
self.state.write() lock which can deadlock; instead, inside identity_sync.rs
(around the code using state.get(&identity_id), existing_row, sentinel, and cs)
clone or take a snapshot of the changeset (cs) and any needed existing_row data
while holding the write lock, then drop the lock before calling
self.persister.store(sentinel, cs_snapshot), and after store returns reacquire
the write lock and re-check that the identity still exists
(state.get(&identity_id)) before applying any cache updates or mutations; ensure
you handle and log persister.store errors the same way but performed outside the
lock.
🧹 Nitpick comments (2)
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift (2)

149-191: ⚡ Quick win

Add a non-matching wallet control for the wallet-scoped key sweep.

This only proves that the targeted walletId row is removed. Because deleteAllIdentityPrivateKeys(forWalletId:) enumerates the entire service, a broken filter that deletes every identity_privkey.* item would still satisfy this test. Seed a second row with a different walletId and assert it survives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift`
around lines 149 - 191, The test testThrowingKeychainSweepsUseIsolatedService
currently only inserts one identity private key so
deleteAllIdentityPrivateKeys(forWalletId:) could accidentally delete all
identity_privkey.* rows; insert a second identity private key via
KeychainManager.storeIdentityPrivateKey with a different walletId (e.g.,
walletId2 and publicKey2) before calling
manager.deleteAllIdentityPrivateKeys(forWalletId: walletId) and after the
deletion assert that retrieveIdentityPrivateKey(publicKeyHex: publicKey2) still
returns non-nil while the original publicKey is nil, ensuring the sweep is
scoped to the targeted walletId.

38-120: ⚡ Quick win

Exercise a transaction that becomes orphaned because of the wallet cascade.

orphanTx starts out orphaned before deletion, so this test still passes if deleteWalletData(walletId:) misses transactions that only become orphaned after wallet/account/TXO rows are removed. Please add one wallet-owned TXO/transaction pair and assert that the transaction is swept after the delete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift`
around lines 38 - 120, Add a transaction+TXO pair that is initially owned by the
wallet and therefore removed by the wallet cascade: create a new
PersistentTransaction named walletOwnedTx and a matching PersistentTxo named
walletOwnedTxo that is associated with the wallet (set whichever field ties a
txo to a wallet in your model—e.g., walletId or an address/ownership field) and
append walletOwnedTxo to walletOwnedTx.outputs, insert both into the context
alongside the existing objects before saving, then after calling
PlatformWalletPersistenceHandler.deleteWalletData(walletId:) assert that the
fetched PersistentTransaction list no longer contains walletOwnedTx (i.e.,
transactions count remains 1 and walletOwnedTx is gone). This ensures
transactions that only become orphaned by the wallet cascade are swept.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-platform-wallet-ffi/src/persistence.rs`:
- Around line 416-422: store() currently only checks the retired set once and
can persist changes if retire_wallet() runs concurrently, allowing stale writes
for just-retired wallets; modify the persistence path to coordinate retirement
and writes atomically by introducing a per-wallet lifecycle lock or state
machine (e.g., a per-wallet mutex/atomic state) and use it in both store() and
retire_wallet() so that store() acquires the wallet lock/state before running
the callback and verifies retirement state again (or aborts) before committing
pending merges; ensure you reference and protect the same retired set and the
code paths that examine changeset.wallet_metadata and perform pending merges so
no post-retire writes can be committed.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 349-353: The deleteWallet(walletId:) flow calls
persistenceHandler?.identityIdsForWallet(...) and proceeds to call
KeychainManager.shared.deleteAllIdentityPrivateKeys(forWalletId:) even when
persistenceHandler is nil (e.g., configure(..., modelContainer: nil)), which can
leave private keys in Keychain; update deleteWallet(walletId:) to guard that
persistenceHandler is present and identityIds were resolved before performing
any destructive Keychain operations—either make persistenceHandler mandatory for
this path (throw an error if nil) or explicitly fail early when identityIds
cannot be retrieved, using the existing persistenceHandler and
identityIdsForWallet(...) check to decide whether to proceed with
KeychainManager.shared.deleteAllKeychainItems(...) and
deleteAllIdentityPrivateKeys(...).

---

Outside diff comments:
In `@packages/rs-platform-wallet/src/manager/identity_sync.rs`:
- Around line 570-587: Currently persister.store(...) is called while holding
the self.state.write() lock which can deadlock; instead, inside identity_sync.rs
(around the code using state.get(&identity_id), existing_row, sentinel, and cs)
clone or take a snapshot of the changeset (cs) and any needed existing_row data
while holding the write lock, then drop the lock before calling
self.persister.store(sentinel, cs_snapshot), and after store returns reacquire
the write lock and re-check that the identity still exists
(state.get(&identity_id)) before applying any cache updates or mutations; ensure
you handle and log persister.store errors the same way but performed outside the
lock.

---

Nitpick comments:
In
`@packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift`:
- Around line 149-191: The test testThrowingKeychainSweepsUseIsolatedService
currently only inserts one identity private key so
deleteAllIdentityPrivateKeys(forWalletId:) could accidentally delete all
identity_privkey.* rows; insert a second identity private key via
KeychainManager.storeIdentityPrivateKey with a different walletId (e.g.,
walletId2 and publicKey2) before calling
manager.deleteAllIdentityPrivateKeys(forWalletId: walletId) and after the
deletion assert that retrieveIdentityPrivateKey(publicKeyHex: publicKey2) still
returns non-nil while the original publicKey is nil, ensuring the sweep is
scoped to the targeted walletId.
- Around line 38-120: Add a transaction+TXO pair that is initially owned by the
wallet and therefore removed by the wallet cascade: create a new
PersistentTransaction named walletOwnedTx and a matching PersistentTxo named
walletOwnedTxo that is associated with the wallet (set whichever field ties a
txo to a wallet in your model—e.g., walletId or an address/ownership field) and
append walletOwnedTxo to walletOwnedTx.outputs, insert both into the context
alongside the existing objects before saving, then after calling
PlatformWalletPersistenceHandler.deleteWalletData(walletId:) assert that the
fetched PersistentTransaction list no longer contains walletOwnedTx (i.e.,
transactions count remains 1 and walletOwnedTx is gone). This ensures
transactions that only become orphaned by the wallet cascade are swept.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 755ba89b-4903-4bcd-a4b0-33c76ed16508

📥 Commits

Reviewing files that changed from the base of the PR and between dfb6d84 and 99c5287.

📒 Files selected for processing (11)
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet/src/changeset/traits.rs
  • packages/rs-platform-wallet/src/manager/identity_sync.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/KeyManagerTests.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift

Comment thread packages/rs-platform-wallet-ffi/src/persistence.rs Outdated
@llbartekll llbartekll marked this pull request as ready for review May 17, 2026 17:07
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 17, 2026

Review Gate

Commit: 656eb8cc

  • Debounce: 159m ago (need 30m)

  • CI checks: builds passed, 0/0 tests passed

  • CodeRabbit review: comment found

  • Off-peak hours: peak window (5am-11am PT) — currently 09:22 AM PT Monday

  • Run review now (check to override)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 350-361: The PlatformWalletManager is removing the wallet via
platform_wallet_manager_remove_wallet and updating wallets before retrieving
per-identity IDs, which can leave a partial state if
persistenceHandler.identityIdsForWallet(walletId:) later throws; move the try
persistenceHandler.identityIdsForWallet(walletId: walletId) call to occur before
calling platform_wallet_manager_remove_wallet and before
wallets.removeValue(forKey:), so identityIds are resolved (and any errors
surface) prior to the FFI removal and in-memory mutation in the removeWallet
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b3bbda40-bf4e-4618-8b7b-b6176b0cf78a

📥 Commits

Reviewing files that changed from the base of the PR and between 99c5287 and 29d2baf.

📒 Files selected for processing (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- Around line 343-360: owned_identity_ids is currently captured under
self.wallet_manager.read().await and remove_wallet(wallet_id) is called later
under a separate write lock, causing a race where identities can be added
between locks; fix by acquiring the wallet_manager write lock
(self.wallet_manager.write().await) once, snapshot the identities (use the same
accessors: identity_manager.wallet_identities.get(wallet_id) and
IdentityGettersV0::id()), and then call remove_wallet(wallet_id) while still
holding that write lock so the snapshot and removal occur atomically; apply the
same pattern to the similar block at lines 373-377.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 2071-2150: Both identityIdsForWallet and deleteWalletData can run
while a Rust changeset is open and thus see/modify a partial backgroundContext;
to fix, prevent these helpers from running during an open changeset by checking
the same changeset state/lock used by beginChangeset/endChangeset at the start
of each function (e.g. call or inline a small guard like ensureNoOpenChangeset()
or wait on the existing changeset mutex/condition) before executing the existing
onQueue block, using the same shared flag/lock that beginChangeset/endChangeset
manipulate so these methods either wait until the changeset is closed or fail
fast rather than reading/saving the shared backgroundContext mid-changeset
(refer to identityIdsForWallet, deleteWalletData, backgroundContext, onQueue,
beginChangeset, endChangeset).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f25090f-9577-49c3-8779-109d0e730aaf

📥 Commits

Reviewing files that changed from the base of the PR and between 29d2baf and a184811.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift
✅ Files skipped from review due to trivial changes (1)
  • packages/rs-platform-wallet-ffi/src/persistence.rs

Comment thread packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
@llbartekll llbartekll marked this pull request as draft May 18, 2026 09:28
… footprint

Adds `PlatformWalletManager.deleteWallet(walletId:) throws` — one SDK
call that wipes a wallet's complete footprint: Rust manager state,
SwiftData rows (including orphans the `@Relationship` graph doesn't
reach), per-identity Keychain entries, and the network sync state
when no sibling wallet remains.

Closes the data-leakage path where `modelContext.delete(wallet)` alone
left orphan rows (`PersistentTransaction`, `PersistentPendingInput`,
`PersistentTokenBalance`) on the same network for the next wallet to
see — bug from dashwallet-ios that was also present in the SDK's
example app.

Rust:
- New `platform_wallet_manager_remove_wallet` FFI, idempotent on
  missing wallets.
- `PlatformWalletManager::remove_wallet` snapshots the wallet's
  identity ids and unregisters each from `IdentitySyncManager` so
  per-identity token sync stops.
- `IdentitySyncManager::apply_fresh_balances` re-checks the live
  identity state under its write lock before persisting, dropping
  the race where a mid-sync unregister could still emit a stale
  token balance.

Swift:
- `PlatformWalletPersistenceHandler.deleteWalletData` cascades the
  schema's @relationship chain, sweeps `PersistentPendingInput` by
  walletId denorm, sweeps orphan `PersistentTransaction` rows after
  the cascade, drops the network sync state when the last wallet on
  the network is gone.
- `ensureWalletRecord` is non-creating; `persistWalletMetadata` is
  the sole callback that materializes a `PersistentWallet` row.
  Other callbacks that arrive for a wallet whose row doesn't exist
  (post-deletion stale callbacks) silently drop.
- `KeychainManager` adds `deleteAllKeychainItems(forIdentityId:)`
  covering both `privkey_*` and `specialkey_*` schemes, plus
  `deleteAllIdentityPrivateKeys(forWalletId:)` for the wallet-
  derived `identity_privkey.*` rows.

Tests: four Swift unit tests against an in-memory `ModelContainer`
covering cascade + orphan sweep, idempotency under double-call,
network-sync conditional cleanup (both last-wallet and sibling-
remains branches), and the three Keychain sweep schemes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
llbartekll and others added 2 commits May 18, 2026 12:02
…ind-only

`ensureWalletRecord` now does find-or-create again and is used only by
`persistWalletMetadata`. `findWalletRecord` is the non-creating sibling
used by `persistWalletChangeset`, `setWalletName`, and `persistAccount`,
each `guard`ing on the result. Same external behavior; names match the
bodies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the window where a stale `persistIdentityKeys` callback could
re-derive from the still-present mnemonic and write fresh
`identity_privkey.*` entries that the keychain sweep wouldn't see.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@llbartekll llbartekll marked this pull request as ready for review May 18, 2026 10:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift (1)

350-363: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve identity IDs before the destructive Rust removal call.

Line 356 removes the wallet first, but Line 361 can still throw. That leaves a partial wipe path where per-identity keychain cleanup never runs.

💡 Suggested fix
+        let identityIds = try persistenceHandler.identityIdsForWallet(walletId: walletId)
+
         try walletId.withUnsafeBytes { raw in
             guard let base = raw.baseAddress?.assumingMemoryBound(to: FFIByteTuple32.self) else {
                 throw PlatformWalletError.nullPointer(
                     "wallet_id buffer base address was nil"
                 )
             }
             try platform_wallet_manager_remove_wallet(handle, base).check()
         }

         wallets.removeValue(forKey: walletId)
-
-        let identityIds = try persistenceHandler.identityIdsForWallet(walletId: walletId)
         try persistenceHandler.deleteWalletData(walletId: walletId)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`
around lines 350 - 363, The code currently removes the in-memory wallet and then
calls persistence work, but it retrieves identity IDs after the destructive Rust
call/wallet removal; fetch the identity IDs first via
persistenceHandler.identityIdsForWallet(walletId:), then perform the
platform_wallet_manager_remove_wallet call and
persistenceHandler.deleteWalletData, and only after all operations succeed
remove the entry from the wallets dictionary (wallets.removeValue(forKey:
walletId)); in short, call identityIdsForWallet before
platform_wallet_manager_remove_wallet (and ensure wallets.removeValue runs last)
so per-identity cleanup can run even if later steps throw.
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift (1)

2063-2143: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Block wallet-deletion helpers during an open changeset.

Line 2063 and Line 2076 can execute while inChangeset == true, which means Line 2137 (save) / Line 2139 (rollback) can commit or discard another round’s partial callback writes.

💡 Suggested fix
+    private func ensureNoOpenChangeset() throws {
+        guard !inChangeset else {
+            throw PlatformWalletError.invalidHandle(
+                "wallet deletion helpers cannot run during an open persistence changeset"
+            )
+        }
+    }
+
     public func identityIdsForWallet(walletId: Data) throws -> [Data] {
         try onQueue {
+            try ensureNoOpenChangeset()
             let descriptor = FetchDescriptor<PersistentWallet>(
                 predicate: PersistentWallet.predicate(walletId: walletId)
             )
             guard let walletRow = try backgroundContext.fetch(descriptor).first else {
                 return []
             }
             return walletRow.identities.map { $0.identityId }
         }
     }

     public func deleteWalletData(walletId: Data) throws {
         try onQueue {
+            try ensureNoOpenChangeset()
             do {
                 // existing body...
                 try backgroundContext.save()
             } catch {
                 backgroundContext.rollback()
                 throw error
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`
around lines 2063 - 2143, Both identityIdsForWallet(walletId:) and
deleteWalletData(walletId:) must not run while inChangeset == true; add an early
guard at the start of their onQueue closures (e.g. guard !inChangeset else {
throw PersistenceError.changesetOpen } or return/throw appropriate error) so the
body never executes when a changeset is open, preventing save()/rollback() from
touching another active changeset; update references in those functions
(identityIdsForWallet and deleteWalletData) to check inChangeset before
fetching/deleting and propagate a clear error (e.g.
PersistenceError.changesetOpen) to callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 350-363: The code currently removes the in-memory wallet and then
calls persistence work, but it retrieves identity IDs after the destructive Rust
call/wallet removal; fetch the identity IDs first via
persistenceHandler.identityIdsForWallet(walletId:), then perform the
platform_wallet_manager_remove_wallet call and
persistenceHandler.deleteWalletData, and only after all operations succeed
remove the entry from the wallets dictionary (wallets.removeValue(forKey:
walletId)); in short, call identityIdsForWallet before
platform_wallet_manager_remove_wallet (and ensure wallets.removeValue runs last)
so per-identity cleanup can run even if later steps throw.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 2063-2143: Both identityIdsForWallet(walletId:) and
deleteWalletData(walletId:) must not run while inChangeset == true; add an early
guard at the start of their onQueue closures (e.g. guard !inChangeset else {
throw PersistenceError.changesetOpen } or return/throw appropriate error) so the
body never executes when a changeset is open, preventing save()/rollback() from
touching another active changeset; update references in those functions
(identityIdsForWallet and deleteWalletData) to check inChangeset before
fetching/deleting and propagate a clear error (e.g.
PersistenceError.changesetOpen) to callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a55c114-8ccd-4c70-ba05-4e14f7f407b3

📥 Commits

Reviewing files that changed from the base of the PR and between a184811 and 81fe665.

📒 Files selected for processing (10)
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet/src/manager/identity_sync.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/KeyManagerTests.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/WalletDeletionTests.swift
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/WalletDetailView.swift
  • packages/rs-platform-wallet/src/manager/identity_sync.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/Security/KeychainManager.swift

- Swift: resolve identity ids before the FFI removal so a SwiftData
  fetch failure surfaces before the destructive ops run.
- Rust: hold the wallet_manager write lock across the identity
  snapshot and the wallet removal so a concurrent identity update
  can't leak a registration past unregister_identity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@llbartekll llbartekll marked this pull request as draft May 18, 2026 10:59
The orphan-tx sweep ran on stale in-memory relationship arrays, so
transactions whose TXOs had just been cascade-deleted (or direct-deleted)
weren't detected as orphan. Two changes together fix it:

- Direct-delete `PersistentTxo` rows by `walletId` denorm before the
  wallet cascade so floating TXOs with no `coreAddress` link are
  still reached.
- Save after the destructive deletes and before the orphan-tx sweep
  so `tx.outputs` / `tx.inputs` / `tx.pendingInputs` reflect the
  committed state rather than the pending one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@llbartekll llbartekll marked this pull request as ready for review May 18, 2026 12:20
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift`:
- Around line 350-366: The code deletes SwiftData wallet rows before sweeping
identity-scoped keychain items, which can leave a permanent partial wipe if
keychain deletion throws; to fix, preserve the identityIds and perform
KeychainManager.shared.deleteAllKeychainItems(forIdentityId:) for each identity
first, ensuring any thrown error aborts before removing persistence, then call
try persistenceHandler.deleteWalletData(walletId:) only after the per-identity
loop completes successfully (also keep wallets.removeValue(forKey: walletId) and
platform_wallet_manager_remove_wallet(handle, ...) ordering consistent), so move
the persistenceHandler.deleteWalletData call below the for identityIds loop and
let errors propagate to avoid inconsistent state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a25c82b6-2b16-4b7a-becc-e9239598f99d

📥 Commits

Reviewing files that changed from the base of the PR and between 81fe665 and bf6ebca.

📒 Files selected for processing (3)
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift

@llbartekll llbartekll merged commit 36c5908 into v3.1-dev May 18, 2026
15 checks passed
@llbartekll llbartekll deleted the delete-wallet branch May 18, 2026 16:28
@PastaPastaPasta PastaPastaPasta mentioned this pull request May 18, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants